#include "article-convert.h"

#include <sstream>

#include "util/strutil.h"
#include "util/imgutil.h"
#include "util/logger/logger.h"
#include "util/char-input-stream.h"
#include "util/html/escape.h"


#include "book/article-menu-convert.h" // TODO do not use 'book' module from 'markdown' module
#include "paragraph-convert.h"


namespace
{

class MdListItem
{
public:

    enum class Type
    {
        Invalid, Unordered, Ordered,
    };

    MdListItem() {}

    MdListItem(const Type &type, int32_t level, const std::string_view &str)
        : type_(type), level_(level), content_(str) {
    }

    const Type &type() const {
        return type_;
    }

    const char *htmlTagName() const {
        switch (type_) {
        case Type::Ordered:
            return "ol";
        case Type::Unordered:
            return "ul";
        default:
            return "";
        }
    }

    int32_t level() const {
        return level_;
    }

    const std::string_view &content() const {
        return content_;
    }

    operator bool() const {
        return type_ != Type::Invalid;
    }

private:
    Type type_ = Type::Invalid;
    int32_t level_ = 0;
    std::string_view content_;
};

static MdListItem tryParseLineAsList(const std::string_view &line) {
    using Size = decltype(line.length());
    const Size len = line.length();
    if (len == 0) {
        return MdListItem();
    }

    enum class State
    {
        Initial, Ordered, Unorderd,
    };

    size_t level = 0;

    State st = State::Initial;

    for (Size i = 0; i < len - 1; ++i) {
        const char c = line[i];
        switch (st) {
        case State::Initial:
            if (strutil::isblank(c)) {
                // do nothing
            } else if (c == '*' || c == '+' || c == '-') {
                st = State::Unorderd;
                level = i;
            } else if (strutil::isdigit(c)) {
                st = State::Ordered;
                level = i;
            }
            break;
        case State::Unorderd:
            if (strutil::isblank(c)) {
                return MdListItem(MdListItem::Type::Unordered, level, strutil::trim(line.substr(i + 1)));
            } else {
                return MdListItem();
            }
            break;
        case State::Ordered:
            if (strutil::isdigit(c)) {
                // do nothing
            } else if (c == '.') {
                if (strutil::isblank(line[i + 1])) {
                    return MdListItem(MdListItem::Type::Ordered, level, strutil::trim(line.substr(i + 1)));
                } else {
                    return MdListItem();
                }
            } else {
                return MdListItem();
            }
            break;
        default:
            break;
        }
    }
    return MdListItem();
}

static const std::string_view kCodeAreaMdTag = "```";

} // namespace anonymous

static std::string makeTitle(const std::string_view &name) {
    std::ostringstream ss;
    ss << "ArticleConvert(" << name << ") ";
    return ss.str();
}

static bool isImg(const std::string_view &line) {
    return strutil::startWith(line, "![](") && strutil::endWith(line, ")");
}

static std::string_view getMdImgUri(const std::string_view &line) {
    if (isImg(line)) {
        return line.substr(4, line.length() - 5);
    } else {
        return std::string_view();
    }
}

namespace book
{

ArticleConvert::End ArticleConvert::end;

ArticleConvert::ArticleConvert(const ConvertRoute &route, const IndexItem &item)
    : title_(makeTitle(item.uriName()))
    , route_(route)
    , item_(item)
    , menuBuilder_(menu_) {
}

void ArticleConvert::operator()() {
    LOGD << title() << "convert begin";

    fs::create_directories(route_.dest());

    in_.open(route_.src(item_.dirEntryName()).generic_string(), std::ios::binary);
    if (!in_) {
        LOGE << title() << "cannot open input file";
        return;
    }

    contentOut_.open(route_.dest(item_.uriName() + ".content.html").generic_string(), std::ios::binary);
    if (!contentOut_) {
        LOGE << title() << "cannot open content output file";
        return;
    }

    menuOut_.open(route_.dest(item_.uriName() + ".menu.html").generic_string(), std::ios::binary);
    if (!menuOut_) {
        LOGE << title() << "cannot open menu output file";
        return;
    }

    doConvertContent();

    ArticleMenuConvert menuConvert(menu_, menuOut_);
    menuConvert();
}

void ArticleConvert::doConvertContent() {
    while (handleSection()) {}
}

bool ArticleConvert::handleSection() {
    sectionBegin();

    bool topTitleFound = false;

    while (std::optional<std::string> pline = reader_.next()) {
        std::string &line = *pline;

        // 遇到一级标题则本节结束
        if (strutil::firstWord(line) == "#") {
            if (topTitleFound) {
                reader_.putback(std::move(line));
                sectionStop();
                return true;
            }

            topTitleFound = true;
        }

        // 处理小节内的内容
        dispatch(line);
    }

    sectionStop();
    //meetNotList();

    return false;
}

static const std::map<std::string, int32_t> g_mdTitleToLevel{
    {"#", 1},
    {"##", 2},
    {"###", 3},
    {"####", 4},
    {"#####", 5},
    {"######", 6},
};

static bool isTitle(const std::string_view &tag) {
    return g_mdTitleToLevel.find(std::string(tag)) != g_mdTitleToLevel.end();
}

static bool isUnorderedList(const std::string_view &tag) {
    return tag == "*" || tag == "+" || tag == "-";
}

static bool isOrderedList(const std::string_view &tag) {
    if (tag.empty()) {
        return false;
    }
    if (tag.back() != '.') {
        return false;
    }
    using Size = decltype(tag.size());
    const Size len = tag.size();
    for (Size i = 0; i < len - 1; ++i) {
        if (!strutil::isdigit(tag[i])) {
            return false;
        }
    }
    return true;
}

static bool isList(const std::string_view &tag) {
    return isUnorderedList(tag) || isOrderedList(tag);
}

static bool isCodeArea(const std::string_view &tag) {
    return strutil::startWith(tag, kCodeAreaMdTag);
}

static bool isReference(const std::string_view &tag) {
    for (char c : tag) {
        if (c != '>') {
            return false;
        }
    }
    return true;
}

static bool isMathBlock(const std::string_view &tag) {
    return tag == "$$";
}

void ArticleConvert::dispatch(const std::string_view &line) {
    const std::string_view tag = strutil::firstWord(line);

    if (isTitle(tag)) {
        handleTitle(line);
        return;
    }

    if (isList(tag)) {
        handleList(line);
        return;
    }

    if (isImg(line)) {
        handleImg(line);
        return;
    }

    if (isCodeArea(tag)) {
        handleCodeArea(line);
        return;
    }

    if (isReference(tag)) {
        handleReference(line);
        return;
    }

    if (isMathBlock(tag)) {
        handleMathBlock(line);
        return;
    }

    handleParagraph(line);
}

void ArticleConvert::handleTitle(const std::string_view &line) {
    const strutil::strviews parts = strutil::split(line, ' ', 2);
    const std::string tag(parts[0]);
    const std::string_view content(parts[1]);

    const auto it = g_mdTitleToLevel.find(tag);
    if (it == g_mdTitleToLevel.end()) {
        LOGE << title() << "unknown markdown tag in line [" << line << "]";
        return;
    }

    if (content.empty()) {
        LOGE << title() << "no title content in line [" << line << "]";
        return;
    }

    const int32_t level = it->second;
    meetTitle(level, content);
}

void ArticleConvert::handleList(const std::string_view &line) {
    Stack<MdListItem> stack;
    MdListItem firstItem = tryParseLineAsList(line);
    if (!firstItem) {
        return;
    }

    stack.push(firstItem);
    contentOut_ << e(firstItem.htmlTagName());
    contentOut_ << e("li");
    handleText(firstItem.content());
    contentOut_ << e("li", end);

    while (std::optional<std::string> pline = reader_.next()) {
        std::string &line = *pline;
        const MdListItem item = tryParseLineAsList(line);
        if (!item) {
            reader_.putback(std::move(line));
            break;
        }

        assert(!stack.empty());
        if (stack.empty()) {
            LOGE << title() << "bad markdown list";
            reader_.putback(std::move(line));
            break;
        }

        const MdListItem &topItem = stack.top();
        if (item.level() > topItem.level()) {
            stack.push(item);
            contentOut_ << e(item.htmlTagName());
        } else if (item.level() < topItem.level()) {
            stack.pop();
            contentOut_ << e(topItem.htmlTagName(), end);
        } else {
            if (item.type() != topItem.type()) {
                stack.pop();
                stack.push(item);
                contentOut_ << e(topItem.htmlTagName(), end);
                contentOut_ << e(item.htmlTagName());
            }
        }

        contentOut_ << e("li");
        handleText(item.content());
        contentOut_ << e("li", end);
    }

    while (!stack.empty()) {
        contentOut_ << e(stack.top().htmlTagName(), end);
        stack.pop();
    }

    contentOut_ << e(firstItem.htmlTagName(), end);
}

void ArticleConvert::handleImg(const std::string_view &line) {
    if (!isImg(line)) {
        return;
    }

    const std::string uri(getMdImgUri(line));
    const fs::path imgSrcPath = route_.src(uri);

    // 图片直接使用源目录的，不需要拷贝

    contentOut_ << "<div class='img-container'>";
    contentOut_ << "<svg class='img-placeholder'";

    const imgutil::Size size = imgutil::imgSize(imgSrcPath);
    if (size) {
        contentOut_ << " width='" << size.width() << "'";
        contentOut_ << " height='" << size.height() << "'";
    }

    contentOut_ << " data-src='" << uri << "'>";
    contentOut_ << "</svg>";
    contentOut_ << "</div>";
}

static std::string_view getCodeLang(const std::string_view &tag) {
    if (tag.size() > kCodeAreaMdTag.size()) {
        return tag.substr(kCodeAreaMdTag.size());
    }
    return std::string_view();
}

static const std::map<std::string, std::string> g_mdCodeLangToCssClass{
    {"c++", "language-cpp"},
    {"bash", "language-bash"}
};

static const std::string *getCodeLangCssClass(const std::string &codeLang) {
    const auto it = g_mdCodeLangToCssClass.find(codeLang);
    if (it != g_mdCodeLangToCssClass.end()) {
        return &(it->second);
    } else {
        return nullptr;
    }
}

void ArticleConvert::handleCodeArea(const std::string_view &line) {
    const std::string_view tag = strutil::firstWord(line);
    if (!strutil::startWith(tag, kCodeAreaMdTag)) {
        return;
    }

    const std::string codeLang(getCodeLang(tag));
    const std::string *cssClass = getCodeLangCssClass(codeLang);

    contentOut_ << "<pre class='line-numbers'>";
    if (cssClass) {
        contentOut_ << "<code class=\"" << (*cssClass) << "\">";
    } else {
        contentOut_ << "<code>";
    }

    while (std::optional<std::string> pline = reader_.next()) {
        std::string &line = *pline;
        if (strutil::startWith(strutil::firstWord(line), kCodeAreaMdTag)) {
            break;
        }
        contentOut_ << html::Escape(line) << "\r\n";
    }

    contentOut_ << "</code></pre>";
}

static std::string_view referenceContent(const std::string_view &line) {
    return strutil::split(line, ' ', 2)[1];
}

void ArticleConvert::handleReference(const std::string_view &line) {
    // 目前只处理最简单的单层且内容仅为段落的引用，这样逻辑简单，目前够用
    // 如果要实现通用的引用处理逻辑，引用的处理逻辑应该包裹在诸如段落、图片、表格等其它元素的上层
    if (line.empty()) {
        return;
    }
    const char *const tagName = "blockquote";
    contentOut_ << "<" << tagName << ">";
    handleParagraph(referenceContent(line));

    while (std::optional<std::string> pline = reader_.next()) {
        std::string &line = *pline;
        if (isReference(strutil::firstWord(line))) {
            handleParagraph(referenceContent(line));
        } else {
            reader_.putback(std::move(line));
            break;
        }
    }
    contentOut_ << "</" << tagName << ">";
}

void ArticleConvert::handleMathBlock(const std::string_view &line) {
    contentOut_ << "<p class='math-block'>";
    contentOut_ << "$$";
    while (std::optional<std::string> pline = reader_.next()) {
        std::string &line = *pline;
        if (isMathBlock(line)) {
            break;
        } else {
            contentOut_ << line;
        }
    }
    contentOut_ << "$$";
    contentOut_ << "</p>";
}

void ArticleConvert::handleParagraph(const std::string_view &line) {
    if (line.empty()) {
        return;
    }
    contentOut_ << "<p>";
    handleText(line);
    contentOut_ << "</p>";
}

static char firstNonBlankChar(const std::string_view &line) {
    for (char c : line) {
        if (!strutil::isblank(c)) {
            return c;
        }
    }
    return 0;
}

void ArticleConvert::handleText(const std::string_view &line) {
    if (strutil::isblank(line)) {
        return;
    }

    std::ostringstream ss;

    StrViewCharInputStream charInput(line);
    ParagraphConvert convert(charInput, ss);
    convert();
    contentOut_ << ss.str();
}

std::string ArticleConvert::e(const std::string &tag) {
    std::string result;
    result.push_back('<');
    result.append(tag);
    result.push_back('>');
    return result;
}

std::string ArticleConvert::e(const std::string &tag, End end) {
    std::string result;
    result.push_back('<');
    result.push_back('/');
    result.append(tag);
    result.push_back('>');
    return result;
}

static const char *sectionLevelToTag(int32_t level) {
    switch (level) {
    case 1: return "h1";
    case 2: return "h2";
    case 3: return "h3";
    case 4: return "h4";
    case 5: return "h5";
    default: return "h6";
    }
}

void ArticleConvert::meetTitle(int32_t level, const std::string_view &content) {
    const SectionId sectionId = sectionIdGenerator_();
    menuBuilder_.add(level, ArticleMenuItem(sectionId, std::string(content)));
    const char *tag = sectionLevelToTag(level);
    contentOut_ << "<" << tag << " id=\"sec" << sectionId << "\" class=\"my-section-title\">";
    handleText(content);
    contentOut_ << "</" << tag << ">";
}

void ArticleConvert::sectionBegin() {
    contentOut_ << "<section>";
}

void ArticleConvert::sectionStop() {
    contentOut_ << "</section>";
}
} // namespace book
